<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>chunk_analyzer 模块评估报告 - 3d2f29</title>
  <style>
    body { font-family: Arial, sans-serif; line-height: 1.6; margin: 0; padding: 20px; color: #333; }
    h1, h2, h3 { color: #2c3e50; }
    .container { max-width: 1200px; margin: 0 auto; }
    .card { background: #fff; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin-bottom: 20px; padding: 20px; }
    .success { color: #27ae60; }
    .error { color: #e74c3c; }
    .info { color: #3498db; }
    table { width: 100%; border-collapse: collapse; margin: 10px 0; }
    th, td { text-align: left; padding: 12px; border-bottom: 1px solid #eee; }
    th { background-color: #f8f9fa; }
    tr:hover { background-color: #f5f5f5; }
    pre { background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto; }
    .progress-container { width: 100%; background-color: #f1f1f1; border-radius: 5px; }
    .progress-bar { height: 20px; border-radius: 5px; }
    .success-bg { background-color: #4CAF50; }
    .pending-bg { background-color: #2196F3; }
    .error-bg { background-color: #f44336; }
    .tag { display: inline-block; padding: 3px 8px; border-radius: 3px; font-size: 12px; font-weight: bold; }
    .tag-success { background-color: #e8f5e9; color: #2e7d32; }
    .tag-error { background-color: #ffebee; color: #c62828; }
    .tag-pending { background-color: #e3f2fd; color: #1565c0; }
  </style>
</head>
<body>
  <div class="container">
    <h1>chunk_analyzer 模块评估报告</h1>
    
    <div class="card">
      <h2>基本信息</h2>
      <table>
        <tr>
          <td width="150"><strong>模块名称</strong></td>
          <td>chunk_analyzer</td>
        </tr>
        <tr>
          <td><strong>模块类型</strong></td>
          <td>ModuleType.CHUNK_ANALYZER</td>
        </tr>
        <tr>
          <td><strong>提交SHA</strong></td>
          <td>3d2f29c88f9b073cb0fd3b9c7f85430e2170acbb.patch</td>
        </tr>
        <tr>
          <td><strong>目标版本</strong></td>
          <td>9.2.3</td>
        </tr>
        <tr>
          <td><strong>执行时间</strong></td>
          <td>2025-05-22T16:04:20.675082</td>
        </tr>
        <tr>
          <td><strong>状态</strong></td>
          <td>
            <span class="tag tag-success">成功</span>
          </td>
        </tr>
      </table>
    </div>
    
    <div class="card">
      <h2>输入信息</h2>
      <table>
        
                <tr>
                  <td width="200"><strong>原始补丁路径</strong></td>
                  <td>/home/elpsy/workspace/sow/patch-backport/workspace/apache/trafficserver/3d2f29/patches/upstream_3d2f29.patch</td>
                </tr>
                
                <tr>
                  <td width="200"><strong>代码仓库路径</strong></td>
                  <td>/home/elpsy/.cache/port-patch/trafficserver</td>
                </tr>
                
      </table>
    </div>
    
    <div class="card">
      <h2>输出信息</h2>
      <table>
        
                <tr>
                  <td width="200"><strong>状态</strong></td>
                  <td>成功</td>
                </tr>
                
                <tr>
                  <td width="200"><strong>成功次数</strong></td>
                  <td>1</td>
                </tr>
                
                <tr>
                  <td width="200"><strong>总尝试次数</strong></td>
                  <td>1</td>
                </tr>
                
                <tr>
                  <td width="200"><strong>执行时间</strong></td>
                  <td>0.406325</td>
                </tr>
                
                <tr>
                  <td width="200"><strong>总块数</strong></td>
                  <td>34</td>
                </tr>
                
                <tr>
                  <td width="200"><strong>成功应用块数</strong></td>
                  <td>3</td>
                </tr>
                
                <tr>
                  <td width="200"><strong>成功率</strong></td>
                  <td>8.82%</td>
                </tr>
                
                <tr>
                  <td width="200"><strong>剩余补丁路径</strong></td>
                  <td>/home/elpsy/workspace/sow/patch-backport/workspace/apache/trafficserver/3d2f29/chunk_patches/remaining_chunks_20250522_160420.patch</td>
                </tr>
                
      </table>
    </div>
    
    
        <div class="card">
          <h2>补丁块应用统计</h2>
          
          <div class="progress-container">
            <div class="progress-bar success-bg" style="width: 8%"></div>
          </div>
          <p>
            成功应用 <strong class="success">3</strong> 个块，
            总共 <strong>34</strong> 个块
            (成功率: <strong>8.8%</strong>)
          </p>
        </div>
        
        <div class="card">
          <h2>补丁内容详情</h2>
          <div class="tabs">
            <div class="tab-header">
              <button class="tab-button active" onclick="openTab(event, 'original-patch')">原始补丁</button>
              <button class="tab-button" onclick="openTab(event, 'applied-patches')">应用成功的补丁</button>
              <button class="tab-button" onclick="openTab(event, 'remaining-patch')">未应用成功的补丁</button>
            </div>
            
            <div id="original-patch" class="tab-content" style="display:block;">
              <h3>原始补丁内容</h3>
              <div class="code-container">
                <pre class="code-block">From 3d2f29c88f9b073cb0fd3b9c7f85430e2170acbb Mon Sep 17 00:00:00 2001
From: Masakazu Kitajo &lt;maskit@apache.org&gt;
Date: Tue, 1 Apr 2025 12:15:16 -0600
Subject: [PATCH] Require the use of CRLF in chunked message body (#12150)

* Require the use of CRLF in chunked message body

* Fix docs
---
 doc/admin-guide/files/records.config.en.rst   |  9 +++
 .../functions/TSHttpOverridableConfig.en.rst  |  1 +
 .../api/types/TSOverridableConfigKey.en.rst   |  1 +
 include/ts/apidefs.h.in                       |  1 +
 mgmt/RecordsConfig.cc                         |  2 +
 plugins/lua/ts_lua_http_config.c              |  2 +
 proxy/http/HttpConfig.cc                      |  2 +
 proxy/http/HttpConfig.h                       |  1 +
 proxy/http/HttpSM.cc                          | 28 +++++---
 proxy/http/HttpTunnel.cc                      | 72 ++++++++++++-------
 proxy/http/HttpTunnel.h                       | 15 +++-
 src/shared/overridable_txn_vars.cc            |  1 +
 src/traffic_server/FetchSM.cc                 |  3 +-
 src/traffic_server/InkAPI.cc                  |  3 +
 src/traffic_server/InkAPITest.cc              |  3 +-
 .../malformed_chunked_header.replay.yaml      | 44 ++++++++++++
 16 files changed, 150 insertions(+), 38 deletions(-)

diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst
index b81510db69d..7db9e6a9f66 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -987,6 +987,15 @@ mptcp
    for details about chunked trailers. By default, this option is disabled
    and therefore |TS| will not drop chunked trailers.
 
+.. ts:cv:: CONFIG proxy.config.http.strict_chunk_parsing INT 1
+   :reloadable:
+   :overridable:
+
+   Specifies whether |TS| strictly checks errors in chunked message body.
+   If enabled (``1``), |TS| returns 400 Bad Request if chunked message body is
+   not compliant with RFC 9112. If disabled (``0``),  |TS| allows using LF as
+   a line terminator.
+
 .. ts:cv:: CONFIG proxy.config.http.send_http11_requests INT 1
    :reloadable:
    :overridable:
diff --git a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
index 2ec29831532..b2b0e231502 100644
--- a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
+++ b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
@@ -111,6 +111,7 @@ TSOverridableConfigKey Value                                              Config
 :c:enumerator:`TS_CONFIG_HTTP_CACHE_WHEN_TO_REVALIDATE`                   :ts:cv:`proxy.config.http.cache.when_to_revalidate`
 :c:enumerator:`TS_CONFIG_HTTP_CHUNKING_ENABLED`                           :ts:cv:`proxy.config.http.chunking_enabled`
 :c:enumerator:`TS_CONFIG_HTTP_CHUNKING_SIZE`                              :ts:cv:`proxy.config.http.chunking.size`
+:c:enumerator:`TS_CONFIG_HTTP_STRICT_CHUNK_PARSING`                       :ts:cv:`proxy.config.http.strict_chunk_parsing`
 :c:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_DEAD_SERVER`   :ts:cv:`proxy.config.http.connect_attempts_max_retries_dead_server`
 :c:enumerator:`TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS`                      :ts:cv:`proxy.config.http.drop_chunked_trailers`
 :c:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES`               :ts:cv:`proxy.config.http.connect_attempts_max_retries`
diff --git a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
index 2d0941efde0..b4291b46579 100644
--- a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
+++ b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
@@ -91,6 +91,7 @@ Enumeration Members
 .. c:enumerator:: TS_CONFIG_NET_SOCK_PACKET_TOS_OUT
 .. c:enumerator:: TS_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE
 .. c:enumerator:: TS_CONFIG_HTTP_CHUNKING_SIZE
+.. c:enumerator:: TS_CONFIG_HTTP_STRICT_CHUNK_PARSING
 .. c:enumerator:: TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS
 .. c:enumerator:: TS_CONFIG_HTTP_FLOW_CONTROL_ENABLED
 .. c:enumerator:: TS_CONFIG_HTTP_FLOW_CONTROL_LOW_WATER_MARK
diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in
index 1641565a1a9..893177c88b9 100644
--- a/include/ts/apidefs.h.in
+++ b/include/ts/apidefs.h.in
@@ -875,6 +875,7 @@ typedef enum {
   TS_CONFIG_HTTP_ENABLE_PARENT_TIMEOUT_MARKDOWNS,
   TS_CONFIG_HTTP_DISABLE_PARENT_MARKDOWNS,
   TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS,
+  TS_CONFIG_HTTP_STRICT_CHUNK_PARSING,
   TS_CONFIG_LAST_ENTRY
 } TSOverridableConfigKey;
 
diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc
index ff7fdc0e3c8..e645bb6c6f1 100644
--- a/mgmt/RecordsConfig.cc
+++ b/mgmt/RecordsConfig.cc
@@ -363,6 +363,8 @@ static const RecordElement RecordsConfig[] =
   ,
   {RECT_CONFIG, "proxy.config.http.drop_chunked_trailers", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, "[0-1]", RECA_NULL}
   ,
+  {RECT_CONFIG, "proxy.config.http.strict_chunk_parsing", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_NULL, "[0-1]", RECA_NULL}
+  ,
   {RECT_CONFIG, "proxy.config.http.flow_control.enabled", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
   ,
   {RECT_CONFIG, "proxy.config.http.flow_control.high_water", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
diff --git a/plugins/lua/ts_lua_http_config.c b/plugins/lua/ts_lua_http_config.c
index a25d8ab8c8f..4b22ee94b50 100644
--- a/plugins/lua/ts_lua_http_config.c
+++ b/plugins/lua/ts_lua_http_config.c
@@ -149,6 +149,7 @@ typedef enum {
   TS_LUA_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE = TS_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE,
   TS_LUA_CONFIG_ENABLE_PARENT_TIMEOUT_MARKDOWNS        = TS_CONFIG_HTTP_ENABLE_PARENT_TIMEOUT_MARKDOWNS,
   TS_LUA_CONFIG_DISABLE_PARENT_MARKDOWNS               = TS_CONFIG_HTTP_DISABLE_PARENT_MARKDOWNS,
+  TS_LUA_CONFIG_HTTP_STRICT_CHUNK_PARSING              = TS_CONFIG_HTTP_STRICT_CHUNK_PARSING,
   TS_LUA_CONFIG_LAST_ENTRY                             = TS_CONFIG_LAST_ENTRY,
 } TSLuaOverridableConfigKey;
 
@@ -290,6 +291,7 @@ ts_lua_var_item ts_lua_http_config_vars[] = {
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_ENABLE_PARENT_TIMEOUT_MARKDOWNS),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_DISABLE_PARENT_MARKDOWNS),
+  TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_STRICT_CHUNK_PARSING),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_LAST_ENTRY),
 };
 
diff --git a/proxy/http/HttpConfig.cc b/proxy/http/HttpConfig.cc
index d5c1c00a283..ca2edee1ee7 100644
--- a/proxy/http/HttpConfig.cc
+++ b/proxy/http/HttpConfig.cc
@@ -1190,6 +1190,7 @@ HttpConfig::startup()
   HttpEstablishStaticConfigByte(c.oride.chunking_enabled, "proxy.config.http.chunking_enabled");
   HttpEstablishStaticConfigLongLong(c.oride.http_chunking_size, "proxy.config.http.chunking.size");
   HttpEstablishStaticConfigByte(c.oride.http_drop_chunked_trailers, "proxy.config.http.drop_chunked_trailers");
+  HttpEstablishStaticConfigByte(c.oride.http_strict_chunk_parsing, "proxy.config.http.strict_chunk_parsing");
   HttpEstablishStaticConfigByte(c.oride.flow_control_enabled, "proxy.config.http.flow_control.enabled");
   HttpEstablishStaticConfigLongLong(c.oride.flow_high_water_mark, "proxy.config.http.flow_control.high_water");
   HttpEstablishStaticConfigLongLong(c.oride.flow_low_water_mark, "proxy.config.http.flow_control.low_water");
@@ -1496,6 +1497,7 @@ HttpConfig::reconfigure()
   params-&gt;oride.keep_alive_enabled_out      = INT_TO_BOOL(m_master.oride.keep_alive_enabled_out);
   params-&gt;oride.chunking_enabled            = INT_TO_BOOL(m_master.oride.chunking_enabled);
   params-&gt;oride.http_drop_chunked_trailers  = m_master.oride.http_drop_chunked_trailers;
+  params-&gt;oride.http_strict_chunk_parsing   = m_master.oride.http_strict_chunk_parsing;
   params-&gt;oride.auth_server_session_private = INT_TO_BOOL(m_master.oride.auth_server_session_private);
 
   params-&gt;oride.http_chunking_size = m_master.oride.http_chunking_size;
diff --git a/proxy/http/HttpConfig.h b/proxy/http/HttpConfig.h
index 6c1763f84e8..53450bdbb25 100644
--- a/proxy/http/HttpConfig.h
+++ b/proxy/http/HttpConfig.h
@@ -703,6 +703,7 @@ struct OverridableHttpConfigParams {
 
   MgmtInt http_chunking_size          = 4096; // Maximum chunk size for chunked output.
   MgmtByte http_drop_chunked_trailers = 0;    ///&lt; Whether to drop chunked trailers.
+  MgmtByte http_strict_chunk_parsing  = 1;    ///&lt; Whether to parse chunked body strictly.
   MgmtInt flow_high_water_mark        = 0;    ///&lt; Flow control high water mark.
   MgmtInt flow_low_water_mark         = 0;    ///&lt; Flow control low water mark.
 
diff --git a/proxy/http/HttpSM.cc b/proxy/http/HttpSM.cc
index cdc05461320..c0ba82641e1 100644
--- a/proxy/http/HttpSM.cc
+++ b/proxy/http/HttpSM.cc
@@ -978,7 +978,8 @@ HttpSM::wait_for_full_body()
   p = tunnel.add_producer(ua_entry-&gt;vc, post_bytes, buf_start, &HttpSM::tunnel_handler_post_ua, HT_BUFFER_READ, "ua post buffer");
   if (chunked) {
     bool const drop_chunked_trailers = t_state.http_config_param-&gt;oride.http_drop_chunked_trailers == 1;
-    tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers);
+    bool const parse_chunk_strictly  = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
+    tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers, parse_chunk_strictly);
   }
   ua_entry-&gt;in_tunnel = true;
   ua_txn-&gt;set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf-&gt;transaction_no_activity_timeout_in));
@@ -6197,10 +6198,11 @@ HttpSM::do_setup_post_tunnel(HttpVC_t to_vc_type)
   // In either case, the server will support chunked (HTTP/1.1)
   if (chunked) {
     bool const drop_chunked_trailers = t_state.http_config_param-&gt;oride.http_drop_chunked_trailers == 1;
+    bool const parse_chunk_strictly  = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
     if (ua_txn-&gt;is_chunked_encoding_supported()) {
-      tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers);
+      tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers, parse_chunk_strictly);
     } else {
-      tunnel.set_producer_chunking_action(p, 0, TCA_CHUNK_CONTENT, drop_chunked_trailers);
+      tunnel.set_producer_chunking_action(p, 0, TCA_CHUNK_CONTENT, drop_chunked_trailers, parse_chunk_strictly);
       tunnel.set_producer_chunking_size(p, 0);
     }
   }
@@ -6609,7 +6611,9 @@ HttpSM::setup_cache_read_transfer()
   // w/o providing a Content-Length header
   if (t_state.client_info.receive_chunked_response) {
     bool const drop_chunked_trailers = t_state.http_config_param-&gt;oride.http_drop_chunked_trailers == 1;
-    tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_CHUNK_CONTENT, drop_chunked_trailers);
+    bool const parse_chunk_strictly  = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
+    tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_CHUNK_CONTENT, drop_chunked_trailers,
+                                        parse_chunk_strictly);
     tunnel.set_producer_chunking_size(p, t_state.txn_conf-&gt;http_chunking_size);
   }
   ua_entry-&gt;in_tunnel    = true;
@@ -6927,8 +6931,10 @@ HttpSM::setup_server_transfer_to_transform()
   transform_info.entry-&gt;in_tunnel = true;
 
   if (t_state.current.server-&gt;transfer_encoding == HttpTransact::CHUNKED_ENCODING) {
-    client_response_hdr_bytes = 0; // fixed by YTS Team, yamsat
-    tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_DECHUNK_CONTENT, HttpTunnel::DROP_CHUNKED_TRAILERS);
+    client_response_hdr_bytes       = 0; // fixed by YTS Team, yamsat
+    bool const parse_chunk_strictly = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
+    tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_DECHUNK_CONTENT, HttpTunnel::DROP_CHUNKED_TRAILERS,
+                                        parse_chunk_strictly);
   }
 
   return p;
@@ -6968,7 +6974,9 @@ HttpSM::setup_transfer_from_transform()
 
   if (t_state.client_info.receive_chunked_response) {
     bool const drop_chunked_trailers = t_state.http_config_param-&gt;oride.http_drop_chunked_trailers == 1;
-    tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_CHUNK_CONTENT, drop_chunked_trailers);
+    bool const parse_chunk_strictly  = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
+    tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_CHUNK_CONTENT, drop_chunked_trailers,
+                                        parse_chunk_strictly);
     tunnel.set_producer_chunking_size(p, t_state.txn_conf-&gt;http_chunking_size);
   }
 
@@ -7025,7 +7033,8 @@ HttpSM::setup_server_transfer_to_cache_only()
     tunnel.add_producer(server_entry-&gt;vc, nbytes, buf_start, &HttpSM::tunnel_handler_server, HT_HTTP_SERVER, "http server");
 
   bool const drop_chunked_trailers = t_state.http_config_param-&gt;oride.http_drop_chunked_trailers == 1;
-  tunnel.set_producer_chunking_action(p, 0, action, drop_chunked_trailers);
+  bool const parse_chunk_strictly  = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
+  tunnel.set_producer_chunking_action(p, 0, action, drop_chunked_trailers, parse_chunk_strictly);
   tunnel.set_producer_chunking_size(p, t_state.txn_conf-&gt;http_chunking_size);
 
   setup_cache_write_transfer(&cache_sm, server_entry-&gt;vc, &t_state.cache_info.object_store, 0, "cache write");
@@ -7114,7 +7123,8 @@ HttpSM::setup_server_transfer()
      }
    */
   bool const drop_chunked_trailers = t_state.http_config_param-&gt;oride.http_drop_chunked_trailers == 1;
-  tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, action, drop_chunked_trailers);
+  bool const parse_chunk_strictly  = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
+  tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, action, drop_chunked_trailers, parse_chunk_strictly);
   tunnel.set_producer_chunking_size(p, t_state.txn_conf-&gt;http_chunking_size);
   return p;
 }
diff --git a/proxy/http/HttpTunnel.cc b/proxy/http/HttpTunnel.cc
index 1508179e6b5..e9c0c6eafea 100644
--- a/proxy/http/HttpTunnel.cc
+++ b/proxy/http/HttpTunnel.cc
@@ -51,27 +51,28 @@ static int const CHUNK_IOBUFFER_SIZE_INDEX = MIN_IOBUFFER_SIZE;
 ChunkedHandler::ChunkedHandler() : max_chunk_size(DEFAULT_MAX_CHUNK_SIZE) {}
 
 void
-ChunkedHandler::init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool drop_chunked_trailers)
+ChunkedHandler::init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool drop_chunked_trailers, bool parse_chunk_strictly)
 {
   if (p-&gt;do_chunking) {
-    init_by_action(buffer_in, ACTION_DOCHUNK, drop_chunked_trailers);
+    init_by_action(buffer_in, ACTION_DOCHUNK, drop_chunked_trailers, parse_chunk_strictly);
   } else if (p-&gt;do_dechunking) {
-    init_by_action(buffer_in, ACTION_DECHUNK, drop_chunked_trailers);
+    init_by_action(buffer_in, ACTION_DECHUNK, drop_chunked_trailers, parse_chunk_strictly);
   } else {
-    init_by_action(buffer_in, ACTION_PASSTHRU, drop_chunked_trailers);
+    init_by_action(buffer_in, ACTION_PASSTHRU, drop_chunked_trailers, parse_chunk_strictly);
   }
   return;
 }
 
 void
-ChunkedHandler::init_by_action(IOBufferReader *buffer_in, Action action, bool drop_chunked_trailers)
+ChunkedHandler::init_by_action(IOBufferReader *buffer_in, Action action, bool drop_chunked_trailers, bool parse_chunk_strictly)
 {
-  running_sum          = 0;
-  num_digits           = 0;
-  cur_chunk_size       = 0;
-  cur_chunk_bytes_left = 0;
-  truncation           = false;
-  this-&gt;action         = action;
+  running_sum                = 0;
+  num_digits                 = 0;
+  cur_chunk_size             = 0;
+  cur_chunk_bytes_left       = 0;
+  truncation                 = false;
+  this-&gt;action               = action;
+  this-&gt;strict_chunk_parsing = parse_chunk_strictly;
 
   switch (action) {
   case ACTION_DOCHUNK:
@@ -139,7 +140,6 @@ ChunkedHandler::read_size()
 {
   int64_t bytes_consumed = 0;
   bool done              = false;
-  int cr                 = 0;
 
   while (chunked_reader-&gt;is_read_avail_more_than(0) && !done) {
     const char *tmp   = chunked_reader-&gt;start();
@@ -178,36 +178,59 @@ ChunkedHandler::read_size()
             done  = true;
             break;
           } else {
-            if (ParseRules::is_cr(*tmp)) {
-              ++cr;
+            if ((prev_is_cr = ParseRules::is_cr(*tmp)) == true) {
+              ++num_cr;
             }
             state = CHUNK_READ_SIZE_CRLF; // now look for CRLF
           }
         }
       } else if (state == CHUNK_READ_SIZE_CRLF) { // Scan for a linefeed
         if (ParseRules::is_lf(*tmp)) {
+          if (!prev_is_cr) {
+            Debug("http_chunk", "Found an LF without a preceding CR (protocol violation)");
+            if (strict_chunk_parsing) {
+              state = CHUNK_READ_ERROR;
+              done  = true;
+              break;
+            }
+          }
           Debug("http_chunk", "read chunk size of %d bytes", running_sum);
           cur_chunk_bytes_left = (cur_chunk_size = running_sum);
           state                = (running_sum == 0) ? CHUNK_READ_TRAILER_BLANK : CHUNK_READ_CHUNK;
           done                 = true;
-          cr                   = 0;
+          num_cr               = 0;
           break;
-        } else if (ParseRules::is_cr(*tmp)) {
-          if (cr != 0) {
+        } else if ((prev_is_cr = ParseRules::is_cr(*tmp)) == true) {
+          if (num_cr != 0) {
             state = CHUNK_READ_ERROR;
             done  = true;
             break;
           }
-          ++cr;
+          ++num_cr;
         }
       } else if (state == CHUNK_READ_SIZE_START) {
-        if (ParseRules::is_cr(*tmp)) {
-          // Skip it
-        } else if (ParseRules::is_lf(*tmp) &&
-                   bytes_used &lt;= 2) { // bytes_used should be 2 if it's CRLF, but permit a single LF as well
+        Debug("http_chunk", "CHUNK_READ_SIZE_START 0x%02x", *tmp);
+        if (ParseRules::is_lf(*tmp)) {
+          if (!prev_is_cr) {
+            Debug("http_chunk", "Found an LF without a preceding CR (protocol violation) before chunk size");
+            if (strict_chunk_parsing) {
+              state = CHUNK_READ_ERROR;
+              done  = true;
+              break;
+            }
+          }
           running_sum = 0;
           num_digits  = 0;
+          num_cr      = 0;
           state       = CHUNK_READ_SIZE;
+        } else if ((prev_is_cr = ParseRules::is_cr(*tmp)) == true) {
+          if (num_cr != 0) {
+            Debug("http_chunk", "Found multiple CRs before chunk size");
+            state = CHUNK_READ_ERROR;
+            done  = true;
+            break;
+          }
+          ++num_cr;
         } else { // Unexpected character
           state = CHUNK_READ_ERROR;
           done  = true;
@@ -651,9 +674,10 @@ HttpTunnel::deallocate_buffers()
 
 void
 HttpTunnel::set_producer_chunking_action(HttpTunnelProducer *p, int64_t skip_bytes, TunnelChunkingAction_t action,
-                                         bool drop_chunked_trailers)
+                                         bool drop_chunked_trailers, bool parse_chunk_strictly)
 {
   this-&gt;http_drop_chunked_trailers = drop_chunked_trailers;
+  this-&gt;http_strict_chunk_parsing  = parse_chunk_strictly;
   p-&gt;chunked_handler.skip_bytes    = skip_bytes;
   p-&gt;chunking_action               = action;
 
@@ -878,7 +902,7 @@ HttpTunnel::producer_run(HttpTunnelProducer *p)
     // For all the chunking cases, we must only copy bytes as we process them.
     body_bytes_to_copy = 0;
 
-    p-&gt;chunked_handler.init(p-&gt;buffer_start, p, this-&gt;http_drop_chunked_trailers);
+    p-&gt;chunked_handler.init(p-&gt;buffer_start, p, this-&gt;http_drop_chunked_trailers, this-&gt;http_strict_chunk_parsing);
 
     // Copy the header into the chunked/dechunked buffers.
     if (p-&gt;do_chunking) {
diff --git a/proxy/http/HttpTunnel.h b/proxy/http/HttpTunnel.h
index 3aac38aca68..9b7d1876425 100644
--- a/proxy/http/HttpTunnel.h
+++ b/proxy/http/HttpTunnel.h
@@ -112,6 +112,8 @@ struct ChunkedHandler {
    */
   bool drop_chunked_trailers = false;
 
+  bool strict_chunk_parsing = true;
+
   bool truncation = false;
 
   /** The number of bytes to skip from the reader because they are not body bytes.
@@ -130,6 +132,8 @@ struct ChunkedHandler {
   // Chunked header size parsing info.
   int running_sum = 0;
   int num_digits  = 0;
+  int num_cr      = 0;
+  bool prev_is_cr = false;
 
   /// @name Output data.
   //@{
@@ -144,8 +148,8 @@ struct ChunkedHandler {
   //@}
   ChunkedHandler();
 
-  void init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool drop_chunked_trailers);
-  void init_by_action(IOBufferReader *buffer_in, Action action, bool drop_chunked_trailers);
+  void init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool drop_chunked_trailers, bool strict_parsing);
+  void init_by_action(IOBufferReader *buffer_in, Action action, bool drop_chunked_trailers, bool strict_parsing);
   void clear();
 
   /// Set the max chunk @a size.
@@ -392,6 +396,7 @@ class HttpTunnel : public Continuation
 
   /// A named variable for the @a drop_chunked_trailers parameter to @a set_producer_chunking_action.
   static constexpr bool DROP_CHUNKED_TRAILERS = true;
+  static constexpr bool PARSE_CHUNK_STRICTLY  = true;
 
   /** Designate chunking behavior to the producer.
    *
@@ -402,9 +407,10 @@ class HttpTunnel : public Continuation
    * @param[in] drop_chunked_trailers If @c true, chunked trailers are filtered
    *   out. Logically speaking, this is only applicable when proxying chunked
    *   content, thus only when @a action is @c TCA_PASSTHRU_CHUNKED_CONTENT.
+   * @param[in] parse_chunk_strictly If @c true, no parse error will be allowed
    */
   void set_producer_chunking_action(HttpTunnelProducer *p, int64_t skip_bytes, TunnelChunkingAction_t action,
-                                    bool drop_chunked_trailers);
+                                    bool drop_chunked_trailers, bool parse_chunk_strictly);
   /// Set the maximum (preferred) chunk @a size of chunked output for @a producer.
   void set_producer_chunking_size(HttpTunnelProducer *producer, int64_t size);
 
@@ -483,6 +489,9 @@ class HttpTunnel : public Continuation
   /// Corresponds to proxy.config.http.drop_chunked_trailers having a value of 1.
   bool http_drop_chunked_trailers = false;
 
+  /// Corresponds to proxy.config.http.strict_chunk_parsing having a value of 1.
+  bool http_strict_chunk_parsing = false;
+
   /** The number of body bytes processed in this last execution of the tunnel.
    *
    * This accounting is used to determine how many bytes to copy into the body
diff --git a/src/shared/overridable_txn_vars.cc b/src/shared/overridable_txn_vars.cc
index 1a5d740794a..f8c6e6e58ea 100644
--- a/src/shared/overridable_txn_vars.cc
+++ b/src/shared/overridable_txn_vars.cc
@@ -31,6 +31,7 @@ const std::unordered_map&lt;std::string_view, std::tuple&lt;const TSOverridableConfigK
      {"proxy.config.http.normalize_ae", {TS_CONFIG_HTTP_NORMALIZE_AE, TS_RECORDDATATYPE_INT}},
      {"proxy.config.http.chunking.size", {TS_CONFIG_HTTP_CHUNKING_SIZE, TS_RECORDDATATYPE_INT}},
      {"proxy.config.http.drop_chunked_trailers", {TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS, TS_RECORDDATATYPE_INT}},
+     {"proxy.config.http.strict_chunk_parsing", {TS_CONFIG_HTTP_STRICT_CHUNK_PARSING, TS_RECORDDATATYPE_INT}},
      {"proxy.config.ssl.client.cert.path", {TS_CONFIG_SSL_CERT_FILEPATH, TS_RECORDDATATYPE_STRING}},
      {"proxy.config.http.allow_half_open", {TS_CONFIG_HTTP_ALLOW_HALF_OPEN, TS_RECORDDATATYPE_INT}},
      {"proxy.config.http.chunking_enabled", {TS_CONFIG_HTTP_CHUNKING_ENABLED, TS_RECORDDATATYPE_INT}},
diff --git a/src/traffic_server/FetchSM.cc b/src/traffic_server/FetchSM.cc
index 19303b7e03d..ad5634845b4 100644
--- a/src/traffic_server/FetchSM.cc
+++ b/src/traffic_server/FetchSM.cc
@@ -198,7 +198,8 @@ FetchSM::check_chunked()
 
     if (resp_is_chunked && (fetch_flags & TS_FETCH_FLAGS_DECHUNK)) {
       ChunkedHandler *ch = &chunked_handler;
-      ch-&gt;init_by_action(resp_reader, ChunkedHandler::ACTION_DECHUNK, HttpTunnel::DROP_CHUNKED_TRAILERS);
+      ch-&gt;init_by_action(resp_reader, ChunkedHandler::ACTION_DECHUNK, HttpTunnel::DROP_CHUNKED_TRAILERS,
+                         HttpTunnel::PARSE_CHUNK_STRICTLY);
       ch-&gt;dechunked_reader = ch-&gt;dechunked_buffer-&gt;alloc_reader();
       ch-&gt;state            = ChunkedHandler::CHUNK_READ_SIZE;
       resp_reader-&gt;dealloc();
diff --git a/src/traffic_server/InkAPI.cc b/src/traffic_server/InkAPI.cc
index 71adb94d0cc..40bc7608ffc 100644
--- a/src/traffic_server/InkAPI.cc
+++ b/src/traffic_server/InkAPI.cc
@@ -8928,6 +8928,9 @@ _conf_to_memberp(TSOverridableConfigKey conf, OverridableHttpConfigParams *overr
   case TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS:
     ret = _memberp_to_generic(&overridableHttpConfig-&gt;http_drop_chunked_trailers, conv);
     break;
+  case TS_CONFIG_HTTP_STRICT_CHUNK_PARSING:
+    ret = _memberp_to_generic(&overridableHttpConfig-&gt;http_strict_chunk_parsing, conv);
+    break;
   case TS_CONFIG_HTTP_FLOW_CONTROL_ENABLED:
     ret = _memberp_to_generic(&overridableHttpConfig-&gt;flow_control_enabled, conv);
     break;
diff --git a/src/traffic_server/InkAPITest.cc b/src/traffic_server/InkAPITest.cc
index a6e7217291a..0e0cac586be 100644
--- a/src/traffic_server/InkAPITest.cc
+++ b/src/traffic_server/InkAPITest.cc
@@ -8774,7 +8774,8 @@ std::array&lt;std::string_view, TS_CONFIG_LAST_ENTRY&gt; SDK_Overridable_Configs = {
    "proxy.config.body_factory.response_suppression_mode",
    "proxy.config.http.parent_proxy.enable_parent_timeout_markdowns",
    "proxy.config.http.parent_proxy.disable_parent_markdowns",
-   "proxy.config.http.drop_chunked_trailers"}};
+   "proxy.config.http.drop_chunked_trailers",
+   "proxy.config.http.strict_chunk_parsing"}};
 
 extern ClassAllocator&lt;HttpSM&gt; httpSMAllocator;
 
diff --git a/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml b/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml
index 1118036b3c8..7c0ccb9a47f 100644
--- a/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml
+++ b/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml
@@ -98,6 +98,27 @@ sessions:
     server-response:
       status: 200
 
+- transactions:
+  - client-request:
+      method: "POST"
+      version: "1.1"
+      url: /malformed/chunk/header3
+      headers:
+        fields:
+        - [ Host, example.com ]
+        - [ Transfer-Encoding, chunked ]
+        - [ uuid, 5 ]
+      content:
+        transfer: plain
+        encoding: uri
+        # Chunk header must end with a sequence of CRLF.
+        data: 7;x%0Aabcwxyz%0D%0A0%0D%0A%0D%0A
+
+    # The connection will be dropped and this response will not go out.
+    server-response:
+      status: 200
+
+
   #
   # Now repeat the above two malformed chunk header tests, but on the server
   # side.
@@ -193,3 +214,26 @@ sessions:
         encoding: uri
         # BWS cannot have CR
         data: 3%0D%0D%0Adef%0D%0A0%0D%0A%0D%0A
+
+- transactions:
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /response/malformed/chunk/size2
+      headers:
+        fields:
+        - [ Host, example.com ]
+        - [ uuid, 105 ]
+
+    # The connection will be dropped and this response will not go out.
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Transfer-Encoding, chunked ]
+      content:
+        transfer: plain
+        encoding: uri
+        # Chunk header must end with a sequence of CRLF.
+        data: 3;x%0Adef%0D%0A0%0D%0A%0D%0A
</pre>
              </div>
            </div>
            
            <div id="applied-patches" class="tab-content">
              <h3>应用成功的补丁内容</h3>
              <div class="code-container">
                <pre class="code-block">

# ===== 应用成功的补丁块 1 =====

From 3d2f29c88f9b073cb0fd3b9c7f85430e2170acbb Mon Sep 17 00:00:00 2001
From: Masakazu Kitajo &lt;maskit@apache.org&gt;
Date: Tue, 1 Apr 2025 12:15:16 -0600
Subject: [PATCH] Require the use of CRLF in chunked message body (#12150)

* Require the use of CRLF in chunked message body

* Fix docs
---
 doc/admin-guide/files/records.config.en.rst   |  9 +++
 .../functions/TSHttpOverridableConfig.en.rst  |  1 +
 .../api/types/TSOverridableConfigKey.en.rst   |  1 +
 include/ts/apidefs.h.in                       |  1 +
 mgmt/RecordsConfig.cc                         |  2 +
 plugins/lua/ts_lua_http_config.c              |  2 +
 proxy/http/HttpConfig.cc                      |  2 +
 proxy/http/HttpConfig.h                       |  1 +
 proxy/http/HttpSM.cc                          | 28 +++++---
 proxy/http/HttpTunnel.cc                      | 72 ++++++++++++-------
 proxy/http/HttpTunnel.h                       | 15 +++-
 src/shared/overridable_txn_vars.cc            |  1 +
 src/traffic_server/FetchSM.cc                 |  3 +-
 src/traffic_server/InkAPI.cc                  |  3 +
 src/traffic_server/InkAPITest.cc              |  3 +-
 .../malformed_chunked_header.replay.yaml      | 44 ++++++++++++
 16 files changed, 150 insertions(+), 38 deletions(-)

diff --git a/plugins/lua/ts_lua_http_config.c b/plugins/lua/ts_lua_http_config.c
index a25d8ab8c8f..4b22ee94b50 100644
--- a/plugins/lua/ts_lua_http_config.c
+++ b/plugins/lua/ts_lua_http_config.c
@@ -149,6 +149,7 @@ typedef enum {
   TS_LUA_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE = TS_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE,
   TS_LUA_CONFIG_ENABLE_PARENT_TIMEOUT_MARKDOWNS        = TS_CONFIG_HTTP_ENABLE_PARENT_TIMEOUT_MARKDOWNS,
   TS_LUA_CONFIG_DISABLE_PARENT_MARKDOWNS               = TS_CONFIG_HTTP_DISABLE_PARENT_MARKDOWNS,
+  TS_LUA_CONFIG_HTTP_STRICT_CHUNK_PARSING              = TS_CONFIG_HTTP_STRICT_CHUNK_PARSING,
   TS_LUA_CONFIG_LAST_ENTRY                             = TS_CONFIG_LAST_ENTRY,
 } TSLuaOverridableConfigKey;
 


# ===== 应用成功的补丁块 2 =====

From 3d2f29c88f9b073cb0fd3b9c7f85430e2170acbb Mon Sep 17 00:00:00 2001
From: Masakazu Kitajo &lt;maskit@apache.org&gt;
Date: Tue, 1 Apr 2025 12:15:16 -0600
Subject: [PATCH] Require the use of CRLF in chunked message body (#12150)

* Require the use of CRLF in chunked message body

* Fix docs
---
 doc/admin-guide/files/records.config.en.rst   |  9 +++
 .../functions/TSHttpOverridableConfig.en.rst  |  1 +
 .../api/types/TSOverridableConfigKey.en.rst   |  1 +
 include/ts/apidefs.h.in                       |  1 +
 mgmt/RecordsConfig.cc                         |  2 +
 plugins/lua/ts_lua_http_config.c              |  2 +
 proxy/http/HttpConfig.cc                      |  2 +
 proxy/http/HttpConfig.h                       |  1 +
 proxy/http/HttpSM.cc                          | 28 +++++---
 proxy/http/HttpTunnel.cc                      | 72 ++++++++++++-------
 proxy/http/HttpTunnel.h                       | 15 +++-
 src/shared/overridable_txn_vars.cc            |  1 +
 src/traffic_server/FetchSM.cc                 |  3 +-
 src/traffic_server/InkAPI.cc                  |  3 +
 src/traffic_server/InkAPITest.cc              |  3 +-
 .../malformed_chunked_header.replay.yaml      | 44 ++++++++++++
 16 files changed, 150 insertions(+), 38 deletions(-)

diff --git a/plugins/lua/ts_lua_http_config.c b/plugins/lua/ts_lua_http_config.c
index a25d8ab8c8f..4b22ee94b50 100644
--- a/plugins/lua/ts_lua_http_config.c
+++ b/plugins/lua/ts_lua_http_config.c
@@ -290,6 +291,7 @@ ts_lua_var_item ts_lua_http_config_vars[] = {
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_BODY_FACTORY_RESPONSE_SUPPRESSION_MODE),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_ENABLE_PARENT_TIMEOUT_MARKDOWNS),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_DISABLE_PARENT_MARKDOWNS),
+  TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_HTTP_STRICT_CHUNK_PARSING),
   TS_LUA_MAKE_VAR_ITEM(TS_LUA_CONFIG_LAST_ENTRY),
 };
 


# ===== 应用成功的补丁块 3 =====

From 3d2f29c88f9b073cb0fd3b9c7f85430e2170acbb Mon Sep 17 00:00:00 2001
From: Masakazu Kitajo &lt;maskit@apache.org&gt;
Date: Tue, 1 Apr 2025 12:15:16 -0600
Subject: [PATCH] Require the use of CRLF in chunked message body (#12150)

* Require the use of CRLF in chunked message body

* Fix docs
---
 doc/admin-guide/files/records.config.en.rst   |  9 +++
 .../functions/TSHttpOverridableConfig.en.rst  |  1 +
 .../api/types/TSOverridableConfigKey.en.rst   |  1 +
 include/ts/apidefs.h.in                       |  1 +
 mgmt/RecordsConfig.cc                         |  2 +
 plugins/lua/ts_lua_http_config.c              |  2 +
 proxy/http/HttpConfig.cc                      |  2 +
 proxy/http/HttpConfig.h                       |  1 +
 proxy/http/HttpSM.cc                          | 28 +++++---
 proxy/http/HttpTunnel.cc                      | 72 ++++++++++++-------
 proxy/http/HttpTunnel.h                       | 15 +++-
 src/shared/overridable_txn_vars.cc            |  1 +
 src/traffic_server/FetchSM.cc                 |  3 +-
 src/traffic_server/InkAPI.cc                  |  3 +
 src/traffic_server/InkAPITest.cc              |  3 +-
 .../malformed_chunked_header.replay.yaml      | 44 ++++++++++++
 16 files changed, 150 insertions(+), 38 deletions(-)

diff --git a/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml b/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml
index 1118036b3c8..7c0ccb9a47f 100644
--- a/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml
+++ b/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml
@@ -98,6 +98,27 @@ sessions:
     server-response:
       status: 200
 
+- transactions:
+  - client-request:
+      method: "POST"
+      version: "1.1"
+      url: /malformed/chunk/header3
+      headers:
+        fields:
+        - [ Host, example.com ]
+        - [ Transfer-Encoding, chunked ]
+        - [ uuid, 5 ]
+      content:
+        transfer: plain
+        encoding: uri
+        # Chunk header must end with a sequence of CRLF.
+        data: 7;x%0Aabcwxyz%0D%0A0%0D%0A%0D%0A
+
+    # The connection will be dropped and this response will not go out.
+    server-response:
+      status: 200
+
+
   #
   # Now repeat the above two malformed chunk header tests, but on the server
   # side.
</pre>
              </div>
            </div>
            
            <div id="remaining-patch" class="tab-content">
              <h3>未应用成功的补丁内容</h3>
              <div class="code-container">
                <pre class="code-block">From 3d2f29c88f9b073cb0fd3b9c7f85430e2170acbb Mon Sep 17 00:00:00 2001
From: Masakazu Kitajo &lt;maskit@apache.org&gt;
Date: Tue, 1 Apr 2025 12:15:16 -0600
Subject: [PATCH] Require the use of CRLF in chunked message body (#12150)

* Require the use of CRLF in chunked message body

* Fix docs
---
 doc/admin-guide/files/records.config.en.rst   |  9 +++
 .../functions/TSHttpOverridableConfig.en.rst  |  1 +
 .../api/types/TSOverridableConfigKey.en.rst   |  1 +
 include/ts/apidefs.h.in                       |  1 +
 mgmt/RecordsConfig.cc                         |  2 +
 plugins/lua/ts_lua_http_config.c              |  2 +
 proxy/http/HttpConfig.cc                      |  2 +
 proxy/http/HttpConfig.h                       |  1 +
 proxy/http/HttpSM.cc                          | 28 +++++---
 proxy/http/HttpTunnel.cc                      | 72 ++++++++++++-------
 proxy/http/HttpTunnel.h                       | 15 +++-
 src/shared/overridable_txn_vars.cc            |  1 +
 src/traffic_server/FetchSM.cc                 |  3 +-
 src/traffic_server/InkAPI.cc                  |  3 +
 src/traffic_server/InkAPITest.cc              |  3 +-
 .../malformed_chunked_header.replay.yaml      | 44 ++++++++++++
 16 files changed, 150 insertions(+), 38 deletions(-)

diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst
index b81510db69d..7db9e6a9f66 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -987,6 +987,15 @@ mptcp
    for details about chunked trailers. By default, this option is disabled
    and therefore |TS| will not drop chunked trailers.
 
+.. ts:cv:: CONFIG proxy.config.http.strict_chunk_parsing INT 1
+   :reloadable:
+   :overridable:
+
+   Specifies whether |TS| strictly checks errors in chunked message body.
+   If enabled (``1``), |TS| returns 400 Bad Request if chunked message body is
+   not compliant with RFC 9112. If disabled (``0``),  |TS| allows using LF as
+   a line terminator.
+
 .. ts:cv:: CONFIG proxy.config.http.send_http11_requests INT 1
    :reloadable:
    :overridable:
diff --git a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
index 2ec29831532..b2b0e231502 100644
--- a/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
+++ b/doc/developer-guide/api/functions/TSHttpOverridableConfig.en.rst
@@ -111,6 +111,7 @@ TSOverridableConfigKey Value                                              Config
 :c:enumerator:`TS_CONFIG_HTTP_CACHE_WHEN_TO_REVALIDATE`                   :ts:cv:`proxy.config.http.cache.when_to_revalidate`
 :c:enumerator:`TS_CONFIG_HTTP_CHUNKING_ENABLED`                           :ts:cv:`proxy.config.http.chunking_enabled`
 :c:enumerator:`TS_CONFIG_HTTP_CHUNKING_SIZE`                              :ts:cv:`proxy.config.http.chunking.size`
+:c:enumerator:`TS_CONFIG_HTTP_STRICT_CHUNK_PARSING`                       :ts:cv:`proxy.config.http.strict_chunk_parsing`
 :c:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES_DEAD_SERVER`   :ts:cv:`proxy.config.http.connect_attempts_max_retries_dead_server`
 :c:enumerator:`TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS`                      :ts:cv:`proxy.config.http.drop_chunked_trailers`
 :c:enumerator:`TS_CONFIG_HTTP_CONNECT_ATTEMPTS_MAX_RETRIES`               :ts:cv:`proxy.config.http.connect_attempts_max_retries`
diff --git a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
index 2d0941efde0..b4291b46579 100644
--- a/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
+++ b/doc/developer-guide/api/types/TSOverridableConfigKey.en.rst
@@ -91,6 +91,7 @@ Enumeration Members
 .. c:enumerator:: TS_CONFIG_NET_SOCK_PACKET_TOS_OUT
 .. c:enumerator:: TS_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE
 .. c:enumerator:: TS_CONFIG_HTTP_CHUNKING_SIZE
+.. c:enumerator:: TS_CONFIG_HTTP_STRICT_CHUNK_PARSING
 .. c:enumerator:: TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS
 .. c:enumerator:: TS_CONFIG_HTTP_FLOW_CONTROL_ENABLED
 .. c:enumerator:: TS_CONFIG_HTTP_FLOW_CONTROL_LOW_WATER_MARK
diff --git a/include/ts/apidefs.h.in b/include/ts/apidefs.h.in
index 1641565a1a9..893177c88b9 100644
--- a/include/ts/apidefs.h.in
+++ b/include/ts/apidefs.h.in
@@ -875,6 +875,7 @@ typedef enum {
   TS_CONFIG_HTTP_ENABLE_PARENT_TIMEOUT_MARKDOWNS,
   TS_CONFIG_HTTP_DISABLE_PARENT_MARKDOWNS,
   TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS,
+  TS_CONFIG_HTTP_STRICT_CHUNK_PARSING,
   TS_CONFIG_LAST_ENTRY
 } TSOverridableConfigKey;
 
diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc
index ff7fdc0e3c8..e645bb6c6f1 100644
--- a/mgmt/RecordsConfig.cc
+++ b/mgmt/RecordsConfig.cc
@@ -363,6 +363,8 @@ static const RecordElement RecordsConfig[] =
   ,
   {RECT_CONFIG, "proxy.config.http.drop_chunked_trailers", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, "[0-1]", RECA_NULL}
   ,
+  {RECT_CONFIG, "proxy.config.http.strict_chunk_parsing", RECD_INT, "1", RECU_DYNAMIC, RR_NULL, RECC_NULL, "[0-1]", RECA_NULL}
+  ,
   {RECT_CONFIG, "proxy.config.http.flow_control.enabled", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
   ,
   {RECT_CONFIG, "proxy.config.http.flow_control.high_water", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
diff --git a/proxy/http/HttpConfig.cc b/proxy/http/HttpConfig.cc
index d5c1c00a283..ca2edee1ee7 100644
--- a/proxy/http/HttpConfig.cc
+++ b/proxy/http/HttpConfig.cc
@@ -1190,6 +1190,7 @@ HttpConfig::startup()
   HttpEstablishStaticConfigByte(c.oride.chunking_enabled, "proxy.config.http.chunking_enabled");
   HttpEstablishStaticConfigLongLong(c.oride.http_chunking_size, "proxy.config.http.chunking.size");
   HttpEstablishStaticConfigByte(c.oride.http_drop_chunked_trailers, "proxy.config.http.drop_chunked_trailers");
+  HttpEstablishStaticConfigByte(c.oride.http_strict_chunk_parsing, "proxy.config.http.strict_chunk_parsing");
   HttpEstablishStaticConfigByte(c.oride.flow_control_enabled, "proxy.config.http.flow_control.enabled");
   HttpEstablishStaticConfigLongLong(c.oride.flow_high_water_mark, "proxy.config.http.flow_control.high_water");
   HttpEstablishStaticConfigLongLong(c.oride.flow_low_water_mark, "proxy.config.http.flow_control.low_water");
@@ -1496,6 +1497,7 @@ HttpConfig::reconfigure()
   params-&gt;oride.keep_alive_enabled_out      = INT_TO_BOOL(m_master.oride.keep_alive_enabled_out);
   params-&gt;oride.chunking_enabled            = INT_TO_BOOL(m_master.oride.chunking_enabled);
   params-&gt;oride.http_drop_chunked_trailers  = m_master.oride.http_drop_chunked_trailers;
+  params-&gt;oride.http_strict_chunk_parsing   = m_master.oride.http_strict_chunk_parsing;
   params-&gt;oride.auth_server_session_private = INT_TO_BOOL(m_master.oride.auth_server_session_private);
 
   params-&gt;oride.http_chunking_size = m_master.oride.http_chunking_size;
diff --git a/proxy/http/HttpConfig.h b/proxy/http/HttpConfig.h
index 6c1763f84e8..53450bdbb25 100644
--- a/proxy/http/HttpConfig.h
+++ b/proxy/http/HttpConfig.h
@@ -703,6 +703,7 @@ struct OverridableHttpConfigParams {
 
   MgmtInt http_chunking_size          = 4096; // Maximum chunk size for chunked output.
   MgmtByte http_drop_chunked_trailers = 0;    ///&lt; Whether to drop chunked trailers.
+  MgmtByte http_strict_chunk_parsing  = 1;    ///&lt; Whether to parse chunked body strictly.
   MgmtInt flow_high_water_mark        = 0;    ///&lt; Flow control high water mark.
   MgmtInt flow_low_water_mark         = 0;    ///&lt; Flow control low water mark.
 
diff --git a/proxy/http/HttpSM.cc b/proxy/http/HttpSM.cc
index cdc05461320..c0ba82641e1 100644
--- a/proxy/http/HttpSM.cc
+++ b/proxy/http/HttpSM.cc
@@ -978,7 +978,8 @@ HttpSM::wait_for_full_body()
   p = tunnel.add_producer(ua_entry-&gt;vc, post_bytes, buf_start, &HttpSM::tunnel_handler_post_ua, HT_BUFFER_READ, "ua post buffer");
   if (chunked) {
     bool const drop_chunked_trailers = t_state.http_config_param-&gt;oride.http_drop_chunked_trailers == 1;
-    tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers);
+    bool const parse_chunk_strictly  = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
+    tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers, parse_chunk_strictly);
   }
   ua_entry-&gt;in_tunnel = true;
   ua_txn-&gt;set_inactivity_timeout(HRTIME_SECONDS(t_state.txn_conf-&gt;transaction_no_activity_timeout_in));
@@ -6197,10 +6198,11 @@ HttpSM::do_setup_post_tunnel(HttpVC_t to_vc_type)
   // In either case, the server will support chunked (HTTP/1.1)
   if (chunked) {
     bool const drop_chunked_trailers = t_state.http_config_param-&gt;oride.http_drop_chunked_trailers == 1;
+    bool const parse_chunk_strictly  = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
     if (ua_txn-&gt;is_chunked_encoding_supported()) {
-      tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers);
+      tunnel.set_producer_chunking_action(p, 0, TCA_PASSTHRU_CHUNKED_CONTENT, drop_chunked_trailers, parse_chunk_strictly);
     } else {
-      tunnel.set_producer_chunking_action(p, 0, TCA_CHUNK_CONTENT, drop_chunked_trailers);
+      tunnel.set_producer_chunking_action(p, 0, TCA_CHUNK_CONTENT, drop_chunked_trailers, parse_chunk_strictly);
       tunnel.set_producer_chunking_size(p, 0);
     }
   }
@@ -6609,7 +6611,9 @@ HttpSM::setup_cache_read_transfer()
   // w/o providing a Content-Length header
   if (t_state.client_info.receive_chunked_response) {
     bool const drop_chunked_trailers = t_state.http_config_param-&gt;oride.http_drop_chunked_trailers == 1;
-    tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_CHUNK_CONTENT, drop_chunked_trailers);
+    bool const parse_chunk_strictly  = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
+    tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_CHUNK_CONTENT, drop_chunked_trailers,
+                                        parse_chunk_strictly);
     tunnel.set_producer_chunking_size(p, t_state.txn_conf-&gt;http_chunking_size);
   }
   ua_entry-&gt;in_tunnel    = true;
@@ -6927,8 +6931,10 @@ HttpSM::setup_server_transfer_to_transform()
   transform_info.entry-&gt;in_tunnel = true;
 
   if (t_state.current.server-&gt;transfer_encoding == HttpTransact::CHUNKED_ENCODING) {
-    client_response_hdr_bytes = 0; // fixed by YTS Team, yamsat
-    tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_DECHUNK_CONTENT, HttpTunnel::DROP_CHUNKED_TRAILERS);
+    client_response_hdr_bytes       = 0; // fixed by YTS Team, yamsat
+    bool const parse_chunk_strictly = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
+    tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_DECHUNK_CONTENT, HttpTunnel::DROP_CHUNKED_TRAILERS,
+                                        parse_chunk_strictly);
   }
 
   return p;
@@ -6968,7 +6974,9 @@ HttpSM::setup_transfer_from_transform()
 
   if (t_state.client_info.receive_chunked_response) {
     bool const drop_chunked_trailers = t_state.http_config_param-&gt;oride.http_drop_chunked_trailers == 1;
-    tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_CHUNK_CONTENT, drop_chunked_trailers);
+    bool const parse_chunk_strictly  = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
+    tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, TCA_CHUNK_CONTENT, drop_chunked_trailers,
+                                        parse_chunk_strictly);
     tunnel.set_producer_chunking_size(p, t_state.txn_conf-&gt;http_chunking_size);
   }
 
@@ -7025,7 +7033,8 @@ HttpSM::setup_server_transfer_to_cache_only()
     tunnel.add_producer(server_entry-&gt;vc, nbytes, buf_start, &HttpSM::tunnel_handler_server, HT_HTTP_SERVER, "http server");
 
   bool const drop_chunked_trailers = t_state.http_config_param-&gt;oride.http_drop_chunked_trailers == 1;
-  tunnel.set_producer_chunking_action(p, 0, action, drop_chunked_trailers);
+  bool const parse_chunk_strictly  = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
+  tunnel.set_producer_chunking_action(p, 0, action, drop_chunked_trailers, parse_chunk_strictly);
   tunnel.set_producer_chunking_size(p, t_state.txn_conf-&gt;http_chunking_size);
 
   setup_cache_write_transfer(&cache_sm, server_entry-&gt;vc, &t_state.cache_info.object_store, 0, "cache write");
@@ -7114,7 +7123,8 @@ HttpSM::setup_server_transfer()
      }
    */
   bool const drop_chunked_trailers = t_state.http_config_param-&gt;oride.http_drop_chunked_trailers == 1;
-  tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, action, drop_chunked_trailers);
+  bool const parse_chunk_strictly  = t_state.http_config_param-&gt;oride.http_strict_chunk_parsing == 1;
+  tunnel.set_producer_chunking_action(p, client_response_hdr_bytes, action, drop_chunked_trailers, parse_chunk_strictly);
   tunnel.set_producer_chunking_size(p, t_state.txn_conf-&gt;http_chunking_size);
   return p;
 }
diff --git a/proxy/http/HttpTunnel.cc b/proxy/http/HttpTunnel.cc
index 1508179e6b5..e9c0c6eafea 100644
--- a/proxy/http/HttpTunnel.cc
+++ b/proxy/http/HttpTunnel.cc
@@ -51,27 +51,28 @@ static int const CHUNK_IOBUFFER_SIZE_INDEX = MIN_IOBUFFER_SIZE;
 ChunkedHandler::ChunkedHandler() : max_chunk_size(DEFAULT_MAX_CHUNK_SIZE) {}
 
 void
-ChunkedHandler::init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool drop_chunked_trailers)
+ChunkedHandler::init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool drop_chunked_trailers, bool parse_chunk_strictly)
 {
   if (p-&gt;do_chunking) {
-    init_by_action(buffer_in, ACTION_DOCHUNK, drop_chunked_trailers);
+    init_by_action(buffer_in, ACTION_DOCHUNK, drop_chunked_trailers, parse_chunk_strictly);
   } else if (p-&gt;do_dechunking) {
-    init_by_action(buffer_in, ACTION_DECHUNK, drop_chunked_trailers);
+    init_by_action(buffer_in, ACTION_DECHUNK, drop_chunked_trailers, parse_chunk_strictly);
   } else {
-    init_by_action(buffer_in, ACTION_PASSTHRU, drop_chunked_trailers);
+    init_by_action(buffer_in, ACTION_PASSTHRU, drop_chunked_trailers, parse_chunk_strictly);
   }
   return;
 }
 
 void
-ChunkedHandler::init_by_action(IOBufferReader *buffer_in, Action action, bool drop_chunked_trailers)
+ChunkedHandler::init_by_action(IOBufferReader *buffer_in, Action action, bool drop_chunked_trailers, bool parse_chunk_strictly)
 {
-  running_sum          = 0;
-  num_digits           = 0;
-  cur_chunk_size       = 0;
-  cur_chunk_bytes_left = 0;
-  truncation           = false;
-  this-&gt;action         = action;
+  running_sum                = 0;
+  num_digits                 = 0;
+  cur_chunk_size             = 0;
+  cur_chunk_bytes_left       = 0;
+  truncation                 = false;
+  this-&gt;action               = action;
+  this-&gt;strict_chunk_parsing = parse_chunk_strictly;
 
   switch (action) {
   case ACTION_DOCHUNK:
@@ -139,7 +140,6 @@ ChunkedHandler::read_size()
 {
   int64_t bytes_consumed = 0;
   bool done              = false;
-  int cr                 = 0;
 
   while (chunked_reader-&gt;is_read_avail_more_than(0) && !done) {
     const char *tmp   = chunked_reader-&gt;start();
@@ -178,36 +178,59 @@ ChunkedHandler::read_size()
             done  = true;
             break;
           } else {
-            if (ParseRules::is_cr(*tmp)) {
-              ++cr;
+            if ((prev_is_cr = ParseRules::is_cr(*tmp)) == true) {
+              ++num_cr;
             }
             state = CHUNK_READ_SIZE_CRLF; // now look for CRLF
           }
         }
       } else if (state == CHUNK_READ_SIZE_CRLF) { // Scan for a linefeed
         if (ParseRules::is_lf(*tmp)) {
+          if (!prev_is_cr) {
+            Debug("http_chunk", "Found an LF without a preceding CR (protocol violation)");
+            if (strict_chunk_parsing) {
+              state = CHUNK_READ_ERROR;
+              done  = true;
+              break;
+            }
+          }
           Debug("http_chunk", "read chunk size of %d bytes", running_sum);
           cur_chunk_bytes_left = (cur_chunk_size = running_sum);
           state                = (running_sum == 0) ? CHUNK_READ_TRAILER_BLANK : CHUNK_READ_CHUNK;
           done                 = true;
-          cr                   = 0;
+          num_cr               = 0;
           break;
-        } else if (ParseRules::is_cr(*tmp)) {
-          if (cr != 0) {
+        } else if ((prev_is_cr = ParseRules::is_cr(*tmp)) == true) {
+          if (num_cr != 0) {
             state = CHUNK_READ_ERROR;
             done  = true;
             break;
           }
-          ++cr;
+          ++num_cr;
         }
       } else if (state == CHUNK_READ_SIZE_START) {
-        if (ParseRules::is_cr(*tmp)) {
-          // Skip it
-        } else if (ParseRules::is_lf(*tmp) &&
-                   bytes_used &lt;= 2) { // bytes_used should be 2 if it's CRLF, but permit a single LF as well
+        Debug("http_chunk", "CHUNK_READ_SIZE_START 0x%02x", *tmp);
+        if (ParseRules::is_lf(*tmp)) {
+          if (!prev_is_cr) {
+            Debug("http_chunk", "Found an LF without a preceding CR (protocol violation) before chunk size");
+            if (strict_chunk_parsing) {
+              state = CHUNK_READ_ERROR;
+              done  = true;
+              break;
+            }
+          }
           running_sum = 0;
           num_digits  = 0;
+          num_cr      = 0;
           state       = CHUNK_READ_SIZE;
+        } else if ((prev_is_cr = ParseRules::is_cr(*tmp)) == true) {
+          if (num_cr != 0) {
+            Debug("http_chunk", "Found multiple CRs before chunk size");
+            state = CHUNK_READ_ERROR;
+            done  = true;
+            break;
+          }
+          ++num_cr;
         } else { // Unexpected character
           state = CHUNK_READ_ERROR;
           done  = true;
@@ -651,9 +674,10 @@ HttpTunnel::deallocate_buffers()
 
 void
 HttpTunnel::set_producer_chunking_action(HttpTunnelProducer *p, int64_t skip_bytes, TunnelChunkingAction_t action,
-                                         bool drop_chunked_trailers)
+                                         bool drop_chunked_trailers, bool parse_chunk_strictly)
 {
   this-&gt;http_drop_chunked_trailers = drop_chunked_trailers;
+  this-&gt;http_strict_chunk_parsing  = parse_chunk_strictly;
   p-&gt;chunked_handler.skip_bytes    = skip_bytes;
   p-&gt;chunking_action               = action;
 
@@ -878,7 +902,7 @@ HttpTunnel::producer_run(HttpTunnelProducer *p)
     // For all the chunking cases, we must only copy bytes as we process them.
     body_bytes_to_copy = 0;
 
-    p-&gt;chunked_handler.init(p-&gt;buffer_start, p, this-&gt;http_drop_chunked_trailers);
+    p-&gt;chunked_handler.init(p-&gt;buffer_start, p, this-&gt;http_drop_chunked_trailers, this-&gt;http_strict_chunk_parsing);
 
     // Copy the header into the chunked/dechunked buffers.
     if (p-&gt;do_chunking) {
diff --git a/proxy/http/HttpTunnel.h b/proxy/http/HttpTunnel.h
index 3aac38aca68..9b7d1876425 100644
--- a/proxy/http/HttpTunnel.h
+++ b/proxy/http/HttpTunnel.h
@@ -112,6 +112,8 @@ struct ChunkedHandler {
    */
   bool drop_chunked_trailers = false;
 
+  bool strict_chunk_parsing = true;
+
   bool truncation = false;
 
   /** The number of bytes to skip from the reader because they are not body bytes.
@@ -130,6 +132,8 @@ struct ChunkedHandler {
   // Chunked header size parsing info.
   int running_sum = 0;
   int num_digits  = 0;
+  int num_cr      = 0;
+  bool prev_is_cr = false;
 
   /// @name Output data.
   //@{
@@ -144,8 +148,8 @@ struct ChunkedHandler {
   //@}
   ChunkedHandler();
 
-  void init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool drop_chunked_trailers);
-  void init_by_action(IOBufferReader *buffer_in, Action action, bool drop_chunked_trailers);
+  void init(IOBufferReader *buffer_in, HttpTunnelProducer *p, bool drop_chunked_trailers, bool strict_parsing);
+  void init_by_action(IOBufferReader *buffer_in, Action action, bool drop_chunked_trailers, bool strict_parsing);
   void clear();
 
   /// Set the max chunk @a size.
@@ -392,6 +396,7 @@ class HttpTunnel : public Continuation
 
   /// A named variable for the @a drop_chunked_trailers parameter to @a set_producer_chunking_action.
   static constexpr bool DROP_CHUNKED_TRAILERS = true;
+  static constexpr bool PARSE_CHUNK_STRICTLY  = true;
 
   /** Designate chunking behavior to the producer.
    *
@@ -402,9 +407,10 @@ class HttpTunnel : public Continuation
    * @param[in] drop_chunked_trailers If @c true, chunked trailers are filtered
    *   out. Logically speaking, this is only applicable when proxying chunked
    *   content, thus only when @a action is @c TCA_PASSTHRU_CHUNKED_CONTENT.
+   * @param[in] parse_chunk_strictly If @c true, no parse error will be allowed
    */
   void set_producer_chunking_action(HttpTunnelProducer *p, int64_t skip_bytes, TunnelChunkingAction_t action,
-                                    bool drop_chunked_trailers);
+                                    bool drop_chunked_trailers, bool parse_chunk_strictly);
   /// Set the maximum (preferred) chunk @a size of chunked output for @a producer.
   void set_producer_chunking_size(HttpTunnelProducer *producer, int64_t size);
 
@@ -483,6 +489,9 @@ class HttpTunnel : public Continuation
   /// Corresponds to proxy.config.http.drop_chunked_trailers having a value of 1.
   bool http_drop_chunked_trailers = false;
 
+  /// Corresponds to proxy.config.http.strict_chunk_parsing having a value of 1.
+  bool http_strict_chunk_parsing = false;
+
   /** The number of body bytes processed in this last execution of the tunnel.
    *
    * This accounting is used to determine how many bytes to copy into the body
diff --git a/src/shared/overridable_txn_vars.cc b/src/shared/overridable_txn_vars.cc
index 1a5d740794a..f8c6e6e58ea 100644
--- a/src/shared/overridable_txn_vars.cc
+++ b/src/shared/overridable_txn_vars.cc
@@ -31,6 +31,7 @@ const std::unordered_map&lt;std::string_view, std::tuple&lt;const TSOverridableConfigK
      {"proxy.config.http.normalize_ae", {TS_CONFIG_HTTP_NORMALIZE_AE, TS_RECORDDATATYPE_INT}},
      {"proxy.config.http.chunking.size", {TS_CONFIG_HTTP_CHUNKING_SIZE, TS_RECORDDATATYPE_INT}},
      {"proxy.config.http.drop_chunked_trailers", {TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS, TS_RECORDDATATYPE_INT}},
+     {"proxy.config.http.strict_chunk_parsing", {TS_CONFIG_HTTP_STRICT_CHUNK_PARSING, TS_RECORDDATATYPE_INT}},
      {"proxy.config.ssl.client.cert.path", {TS_CONFIG_SSL_CERT_FILEPATH, TS_RECORDDATATYPE_STRING}},
      {"proxy.config.http.allow_half_open", {TS_CONFIG_HTTP_ALLOW_HALF_OPEN, TS_RECORDDATATYPE_INT}},
      {"proxy.config.http.chunking_enabled", {TS_CONFIG_HTTP_CHUNKING_ENABLED, TS_RECORDDATATYPE_INT}},
diff --git a/src/traffic_server/FetchSM.cc b/src/traffic_server/FetchSM.cc
index 19303b7e03d..ad5634845b4 100644
--- a/src/traffic_server/FetchSM.cc
+++ b/src/traffic_server/FetchSM.cc
@@ -198,7 +198,8 @@ FetchSM::check_chunked()
 
     if (resp_is_chunked && (fetch_flags & TS_FETCH_FLAGS_DECHUNK)) {
       ChunkedHandler *ch = &chunked_handler;
-      ch-&gt;init_by_action(resp_reader, ChunkedHandler::ACTION_DECHUNK, HttpTunnel::DROP_CHUNKED_TRAILERS);
+      ch-&gt;init_by_action(resp_reader, ChunkedHandler::ACTION_DECHUNK, HttpTunnel::DROP_CHUNKED_TRAILERS,
+                         HttpTunnel::PARSE_CHUNK_STRICTLY);
       ch-&gt;dechunked_reader = ch-&gt;dechunked_buffer-&gt;alloc_reader();
       ch-&gt;state            = ChunkedHandler::CHUNK_READ_SIZE;
       resp_reader-&gt;dealloc();
diff --git a/src/traffic_server/InkAPI.cc b/src/traffic_server/InkAPI.cc
index 71adb94d0cc..40bc7608ffc 100644
--- a/src/traffic_server/InkAPI.cc
+++ b/src/traffic_server/InkAPI.cc
@@ -8928,6 +8928,9 @@ _conf_to_memberp(TSOverridableConfigKey conf, OverridableHttpConfigParams *overr
   case TS_CONFIG_HTTP_DROP_CHUNKED_TRAILERS:
     ret = _memberp_to_generic(&overridableHttpConfig-&gt;http_drop_chunked_trailers, conv);
     break;
+  case TS_CONFIG_HTTP_STRICT_CHUNK_PARSING:
+    ret = _memberp_to_generic(&overridableHttpConfig-&gt;http_strict_chunk_parsing, conv);
+    break;
   case TS_CONFIG_HTTP_FLOW_CONTROL_ENABLED:
     ret = _memberp_to_generic(&overridableHttpConfig-&gt;flow_control_enabled, conv);
     break;
diff --git a/src/traffic_server/InkAPITest.cc b/src/traffic_server/InkAPITest.cc
index a6e7217291a..0e0cac586be 100644
--- a/src/traffic_server/InkAPITest.cc
+++ b/src/traffic_server/InkAPITest.cc
@@ -8774,7 +8774,8 @@ std::array&lt;std::string_view, TS_CONFIG_LAST_ENTRY&gt; SDK_Overridable_Configs = {
    "proxy.config.body_factory.response_suppression_mode",
    "proxy.config.http.parent_proxy.enable_parent_timeout_markdowns",
    "proxy.config.http.parent_proxy.disable_parent_markdowns",
-   "proxy.config.http.drop_chunked_trailers"}};
+   "proxy.config.http.drop_chunked_trailers",
+   "proxy.config.http.strict_chunk_parsing"}};
 
 extern ClassAllocator&lt;HttpSM&gt; httpSMAllocator;
 
diff --git a/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml b/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml
index 1118036b3c8..7c0ccb9a47f 100644
--- a/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml
+++ b/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml
@@ -193,3 +214,26 @@ sessions:
         encoding: uri
         # BWS cannot have CR
         data: 3%0D%0D%0Adef%0D%0A0%0D%0A%0D%0A
+
+- transactions:
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /response/malformed/chunk/size2
+      headers:
+        fields:
+        - [ Host, example.com ]
+        - [ uuid, 105 ]
+
+    # The connection will be dropped and this response will not go out.
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Transfer-Encoding, chunked ]
+      content:
+        transfer: plain
+        encoding: uri
+        # Chunk header must end with a sequence of CRLF.
+        data: 3;x%0Adef%0D%0A0%0D%0A%0D%0A
</pre>
              </div>
            </div>
          </div>
        </div>
        
                <div class="card">
                  <h2>应用成功的补丁块</h2>
                  <table>
                    <tr>
                      <th>块索引</th>
                      <th>修改文件</th>
                      <th>修改位置</th>
                      <th>操作</th>
                    </tr>
                    
                    <tr>
                      <td>1</td>
                      <td>plugins/lua/ts_lua_http_config.c</td>
                      <td>149-149</td>
                      <td><a href="applied_chunk_1.diff" target="_blank">查看</a></td>
                    </tr>
                    

                    <tr>
                      <td>2</td>
                      <td>plugins/lua/ts_lua_http_config.c</td>
                      <td>290-291</td>
                      <td><a href="applied_chunk_2.diff" target="_blank">查看</a></td>
                    </tr>
                    

                    <tr>
                      <td>3</td>
                      <td>tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml</td>
                      <td>98-98</td>
                      <td><a href="applied_chunk_3.diff" target="_blank">查看</a></td>
                    </tr>
                    
                  </table>
                </div>
                
            <div class="card">
              <h2>剩余补丁</h2>
              <p>有 <strong class="info">31</strong> 个块未成功应用，已生成剩余补丁:</p>
              <p><code>/home/elpsy/workspace/sow/patch-backport/workspace/apache/trafficserver/3d2f29/chunk_patches/remaining_chunks_20250522_160420.patch</code></p>
              <p><a href="remaining_patch.diff" target="_blank">查看剩余补丁</a></p>
            </div>
            
        <style>
        .code-container {
          max-height: 500px;
          overflow-y: auto;
          background-color: #f8f9fa;
          border-radius: 5px;
          border: 1px solid #eee;
        }
        
        .code-block {
          padding: 15px;
          margin: 0;
          white-space: pre-wrap;
          font-family: monospace;
          font-size: 13px;
          line-height: 1.4;
        }
        
        /* 为补丁内容添加语法高亮 */
        .code-block .add {
          background-color: #e6ffed;
          color: #22863a;
        }
        
        .code-block .remove {
          background-color: #ffeef0;
          color: #cb2431;
        }
        
        .code-block .hunk {
          color: #0366d6;
          background-color: #f1f8ff;
        }
        
        .code-block .header {
          color: #6f42c1;
          font-weight: bold;
        }
        </style>
        
        <script>
        // 对补丁内容应用简单的语法高亮
        document.addEventListener('DOMContentLoaded', function() {
          const codeBlocks = document.querySelectorAll('.code-block');
          codeBlocks.forEach(function(block) {
            let html = block.innerHTML;
            
            // 替换添加的行
            html = html.replace(/^(\+[^+].*)/gm, '<span class="add">$1</span>');
            
            // 替换删除的行
            html = html.replace(/^(-[^-].*)/gm, '<span class="remove">$1</span>');
            
            // 替换区块头
            html = html.replace(/^(@@.*@@)/gm, '<span class="hunk">$1</span>');
            
            // 替换diff头 - 修复无效转义序列
            html = html.replace(/^(diff --git.*|index.*|---.*|\+\+\+.*)/gm, '<span class="header">$1</span>');
            
            block.innerHTML = html;
          });
        });
        </script>
        
    
  </div>
</body>
</html>